package timesched

import (
	"context"
	"duanju-mq/pkg/app"
	"errors"
	"strconv"
	"strings"
	"time"
)

type KeyWord struct {
	Year   byte
	Month  byte
	Day    byte
	Hour   byte
	Minute byte
	Second byte
	Week   byte
}

var defaultKeyWord = &KeyWord{
	Year:   'Y',
	Month:  'M',
	Day:    'D',
	Hour:   'h',
	Minute: 'm',
	Second: 's',
	Week:   'W',
}

type Scheduler struct {
	s      string
	rules  []*rule
	cancel context.CancelFunc
}

type rule struct {
	year   map[int]bool
	month  map[int]bool
	day    map[int]bool
	hour   map[int]bool
	minute map[int]bool
	second map[int]bool
	week   map[int]bool
}

func (s *Scheduler) String() string {
	return s.s
}

func Parse(s string) (*Scheduler, error) {
	return ParseWithKeyWord(defaultKeyWord, s)
}

func ParseWithKeyWord(kw *KeyWord, s string) (*Scheduler, error) {
	sch := &Scheduler{s: s, rules: []*rule{}}
	for _, p := range strings.Split(s, "|") {
		p = strings.TrimSpace(p)
		if p == "" {
			continue
		}
		used := map[byte]bool{}
		r := &rule{}
		for i := 0; i < len(p); {
			c := p[i]
			i++
			if c == ' ' {
				continue
			}
			if used[c] {
				return nil, errors.New("duplicated keyword " + string(c))
			}
			switch c {
			case kw.Year:
				r.year, i = parseSeg(p, i)
			case kw.Month:
				r.month, i = parseSeg(p, i)
			case kw.Day:
				r.day, i = parseSeg(p, i)
			case kw.Hour:
				r.hour, i = parseSeg(p, i)
			case kw.Minute:
				r.minute, i = parseSeg(p, i)
			case kw.Second:
				r.second, i = parseSeg(p, i)
			case kw.Week:
				r.week, i = parseSeg(p, i)
			default:
				return nil, errors.New("unexpected keyword " + string(c))
			}
			used[c] = true
		}
		sch.rules = append(sch.rules, r)
	}
	return sch, nil
}

func parseSeg(s string, i int) (map[int]bool, int) {
	j := i
loop:
	for ; j < len(s); j++ {
		switch s[j] {
		case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ',', '-':
		default:
			break loop
		}
	}
	return parseIntMap(s[i:j]), j
}

func parseIntMap(s string) map[int]bool {
	m := map[int]bool{}
	ss := strings.Split(s, ",")
	for _, r := range ss {
		sss := strings.Split(r, "-")
		switch len(sss) {
		case 1:
			n, err := strconv.Atoi(sss[0])
			if err != nil {
				continue
			}
			m[n] = true
		case 2:
			begin, err := strconv.Atoi(sss[0])
			if err != nil {
				continue
			}
			end, err := strconv.Atoi(sss[1])
			if err != nil {
				continue
			}
			for i := begin; i <= end; i++ {
				m[i] = true
			}
		default:
			continue
		}
	}
	return m
}

func (sch *Scheduler) check(Y, M, D, h, m, s, w int) bool {
	for _, r := range sch.rules {
		if r.check(Y, M, D, h, m, s, w) {
			return true
		}
	}
	return false
}

func (r *rule) check(Y, M, D, h, m, s, w int) bool {
	if r.year != nil && len(r.year) > 0 && !r.year[Y] {
		return false
	}
	if r.month != nil && len(r.month) > 0 && !r.month[M] {
		return false
	}
	if r.day != nil && len(r.day) > 0 && !r.day[D] {
		return false
	}
	if r.hour != nil && len(r.hour) > 0 && !r.hour[h] {
		return false
	}
	if r.minute != nil && len(r.minute) > 0 && !r.minute[m] {
		return false
	}
	if r.second != nil && len(r.second) > 0 && !r.second[s] {
		return false
	}
	if r.week != nil && len(r.week) > 0 && !r.week[w] {
		return false
	}
	return true
}

func datetime(t time.Time) (Y, M, D, h, m, s, w int) {
	var mm time.Month
	Y, mm, D = t.Date()
	M = int(mm)
	h, m, s = t.Clock()
	w = int(t.Weekday())
	if w == 0 {
		w = 7
	}
	return
}

func (s *Scheduler) Check(t time.Time) bool {
	return s.check(datetime(t))
}

func (s *Scheduler) Start(fn func(time.Time)) error {
	if s.cancel != nil {
		return errors.New("scheduler is running")
	}
	lastTime := time.Now().Unix()
	ticker := time.NewTicker(time.Second)
	defer ticker.Stop()
	ctx, cancel := context.WithCancel(app.CTX)
	s.cancel = cancel
	defer func() { s.cancel = nil }()
loop:
	for {
		select {
		case <-ctx.Done():
			break loop
		case now := <-ticker.C:
			func(t time.Time) {
				ts := t.Unix()
				for i := lastTime + 1; i <= ts; i++ { //循环防止跳秒
					lastTime = i
					tt := time.Unix(i, int64(t.Nanosecond()))
					if s.Check(tt) {
						app.Async(func() {
							fn(tt)
						})
					}
				}
			}(now)
		}
	}
	return nil
}

func (s *Scheduler) Stop() {
	if s.cancel != nil {
		s.cancel()
	}
}
